package cleveland;
import java.util.HashSet;
import java.util.Set;

import org.atcs.hearts.Card;
import org.atcs.hearts.Card.Rank;
import org.atcs.hearts.Card.Suit;
import org.atcs.hearts.ai.AI;

/**
 * Basic Hearts AI.
 * 
 * @author raphtown
 * @version 1.0
 */
public class Cleveland extends AI
{
	private final int FIRST = 0;
	private final int LAST = 3;
	private Set<Card> cards;
	private boolean heartsBroken = false;
	private Card[] cardsPlayedDuringTrick = new Card[4];
	private int numCardsPlayed = 0;
	Set<Card> cardsYetToBePlayed;
	Set<Card> cardsPlayed;
	Suit suitLed;
	int trickNum = 0;
	int position;
	public double[] total = new double[52];
	public double[] numPlayed = new double[52];
	int indexSP = 0;
	public Opponent[] opponents = new Opponent[4];
	int ownPlacement;

	@Override
	public void cardPlayed(int player, int count, Card card)
	{
		for(int i = 0; i < opponents.length; i++)
		{
			if(i != ownPlacement)
			{
				opponents[i].cardsInHand.remove(card);
			}
		}

		if(player != ownPlacement)
		{
			if(card.suit != suitLed)
			{
				opponents[player].cardsInHand.removeAll(getCardsBySuit(suitLed, opponents[player].cardsInHand));
			}
		}

		cardsPlayed.add(card);
		cardsYetToBePlayed.remove(card);
		numCardsPlayed++;
		cardsPlayedDuringTrick[count] = card;
		if(card.suit == Suit.HEART && !heartsBroken)
		{
			heartsBroken = true;
		}
	}

	@Override
	public void deal(Set<Card> cards)
	{
		this.ownPlacement = this.getID();
		this.cards = cards;
		cardsPlayed = new HashSet<Card>();
		cardsYetToBePlayed = new HashSet<Card>();
		for (Suit suit : Card.Suit.values()) {
			for (Rank rank : Card.Rank.values()) {
				cardsYetToBePlayed.add(new Card(rank, suit));
			}
		}
		cardsYetToBePlayed.removeAll(cards);
	}

	@Override
	public String getEndTrickMessage()
	{
		return "Hello Mojo";
	}

	@Override
	public String getName()
	{
		return "Cleveland";
	}


	public Card[] passCards(int toPlayer)
	{
		int index = 0;
		Card[] cardsToPass = new Card[3];


		if(hasQueenOfSpades(cards))
		{

			if(2 > getCardsLowerThanOrEqualTo(new Card(Rank.QUEEN, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)).size())
			{
				cardsToPass[index] = new Card(Rank.QUEEN, Suit.SPADE);
				cards.remove(new Card(Rank.QUEEN, Suit.SPADE));
				index++;

			}
		}
		else if(getCardsHigherThanOrEqualTo(new Card(Rank.KING, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)) != null)
		{
			for(Card c: getCardsHigherThanOrEqualTo(new Card(Rank.KING, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)))
			{
				cardsToPass[index] = c;
				cards.remove(c);
				index++;
			}
		}

		for(int i = index; i < cardsToPass.length; i++)
		{
			if(cardsToPass[i] == null)
			{
				if(hasQueenOfSpades(cards))
				{
					Card card = getHighestCard(getCardsBySuit(getNonVoidedSuitWithLowestNumberOfCards
							(getAllCardsExceptSuit(Suit.HEART, cards)), cards));
					cards.remove(card);
					cardsToPass[i] = card;
				}
				else
				{
					Card card = getHighestCard(getCardsBySuit(getNonVoidedSuitWithLowestNumberOfCards
							(getAllCardsExceptSuit(Suit.HEART, getAllCardsExceptSuit(Suit.SPADE, cards))), cards));
					cards.remove(card);
					cardsToPass[i] = card;
				}

			}
		}

		for(int i = 0; i < cardsToPass.length; i++)
		{
			if(cardsToPass[i] == null)
			{
				Card card = this.getRandomCard(cards);
				cards.remove(card);
				cardsToPass[i] = card;
			}
		}
		for(int i = 0; i < cardsToPass.length; i++)
		{
			cardsYetToBePlayed.add(cardsToPass[i]);
		}

		for(Opponent o: opponents)
		{
			o = new Opponent(cardsYetToBePlayed);
		}

		return cardsToPass;
	}

	@Override
	public void passedCards(int fromPlayer, Card[] cards)
	{
		for(Card card : cards)
		{
			cardsYetToBePlayed.remove(card);
			this.cards.add(card);
		}


	}

	public Card playCard(int position)
	{
		this.position = position;
		Card cardToPlay;
		if(getAllCardsExceptSuit(Suit.HEART, cards) == null || getAllCardsExceptSuit(Suit.HEART, cards).size() == 0)
		{
			cardToPlay = auxPlayCard(position, cards);
		}
		else if(trickNum == 0)
		{
			cardToPlay = auxPlayCard(position, getAllCardsExceptSuit(Suit.HEART, cardsWithout(new Card(Rank.QUEEN, Suit.SPADE), cards)));
		}
		else if(heartsBroken)
		{
			cardToPlay = auxPlayCard(position, cards);
		}
		else
		{
			cardToPlay = auxPlayCard(position, getAllCardsExceptSuit(Suit.HEART, cards));
		}
		cards.remove(cardToPlay);
		indexSP = indexOf(cardToPlay);
		return cardToPlay;
	}

	public int indexOf(Card c)
	{
		return 13 * c.suit.ordinal() + c.rank.ordinal();
	}

	public Card cardFromIndex(int index)
	{
		return new Card(Rank.getRankFromOrdinal(index % 13), Suit.getSuitFromOrdinal(index / 13));
	}

	private Card auxPlayCard(int position, Set<Card> cardsToSearchThrough)
	{
		// Leads two of Clubs if it has it
		Card twoOfClubs = new Card(Rank.TWO, Suit.CLUB);
		if(position == FIRST && hasCard(twoOfClubs, cardsToSearchThrough))
		{
			return twoOfClubs;
		}
		if(trickNum == 10)
		{
			Set<Card> temp = cardsYetToBePlayed;
			for(int i = position; i < 4; i++)
			{
				if(position != ownPlacement)
				{
					for(int j = 0; j < 3; j++)
					{
						int rand = (int)(Math.random() * opponents[position].cardsInHand.size());
//						opponents[i]
					}
				}
			}

		}
		if(position == FIRST)
		{
			if(!hasQueenOfSpades(cards) 
					&& getCardsLowerThanOrEqualTo(new Card(Rank.QUEEN, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)) != null
					&& getCardsLowerThanOrEqualTo(new Card(Rank.QUEEN, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)).size() > 0)
			{
				return getLowestCard(getCardsLowerThanOrEqualTo(new Card(Rank.QUEEN, Suit.SPADE), getCardsBySuit(Suit.SPADE, cards)));
			}
			Suit target = getNonVoidedSuitWithLowestNumberOfCards(cardsToSearchThrough);
			Card goodCard = getHighestSafeCardToPlay(target, cardsToSearchThrough);

			Set<Suit> otherSuits = new HashSet<Suit>();
			for(Suit suit:Suit.values())
			{
				if(suit != target)
				{
					otherSuits.add(suit);
				}
			}

			for(Suit s:otherSuits)
			{
				if(goodCard != null)
				{
					return goodCard;
				}
				goodCard = getHighestSafeCardToPlay(s, cardsToSearchThrough);
			}

			if(goodCard != null)
			{
				return goodCard;
			}
			Card lowestCard = getLowestCard(cardsToSearchThrough);
			return lowestCard;
		}

		suitLed = cardsPlayedDuringTrick[FIRST].suit;
		if(hasSuit(suitLed, cardsToSearchThrough))
		{
			Card bestCard;
			if(position == LAST && getValueOfTrickSoFar() < 1)
			{
				bestCard = getHighestCard(getCardsBySuit(suitLed, cardsToSearchThrough));
				return bestCard;
			}
			bestCard = getHighestCard(getCardsBySuit(suitLed, 
					getCardsLowerThanOrEqualTo(getHighestCardPlayedOfRightSuit(),
							cardsToSearchThrough)));
			if(bestCard != null)
			{
				return bestCard;
			}
			else
			{
				bestCard = getLowestCard(getCardsBySuit(suitLed, cardsToSearchThrough));
				return bestCard;
			}
		}

		if(hasQueenOfSpades(cardsToSearchThrough))
		{
			Card qos = new Card(Card.Rank.QUEEN, Card.Suit.SPADE);
			return qos;
		}

		if(hasSuit(Suit.HEART, cardsToSearchThrough))
		{
			Card highestHeart = getHighestCard(getCardsBySuit(Suit.HEART, cardsToSearchThrough));
			return highestHeart;
		}


		Card highestCardOfSuitToBeVoided = getHighestCard(getCardsBySuit(getNonVoidedSuitWithLowestNumberOfCards(cardsToSearchThrough), cardsToSearchThrough));
		return highestCardOfSuitToBeVoided;
	}


	@Override
	public void roundEnded(int[] totalPoints)
	{
		cardsPlayed.clear();
		Set<Card> cardsYetToBePlayed = new HashSet<Card>();
		for (Suit suit : Card.Suit.values()) {
			for (Rank rank : Card.Rank.values()) {
				cardsYetToBePlayed.add(new Card(rank, suit));
			}
		}
		heartsBroken = false;	
		trickNum = 0;
	}

	@Override
	public void trickEnded(int player, int points)
	{
		trickNum++;
		cardsPlayedDuringTrick = new Card[4];
		numCardsPlayed = 0;

		if(player == position)
		{
			total[indexSP] = total[indexSP] + points;
		}
		numPlayed[indexSP]++;
	}



	/**
	 * Checks to see if you have a given Card in your hand
	 * 
	 * @param c Card to find
	 * @param cardsToSearch Set of cards to look through
	 * @return true if Card is found, false if not found
	 */
	private boolean hasCard(Card c, Set<Card> cardsToSearch)
	{
		if(cardsToSearch == null || cardsToSearch.size() == 0)
		{
			return false;
		}
		for(Card card:cardsToSearch)
		{
			if(card.equals(c))
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks to see if you have a given Suit in your hand
	 * 
	 * @param s Suit to find
	 * @param cardsToSearch Set of cards to look through
	 * @return true if Suit is found, false if not found
	 */
	private boolean hasSuit(Suit s, Set<Card> cardsToSearch)
	{
		if(cardsToSearch == null || s == null)
		{
			return false;
		}
		for(Card card:cardsToSearch)
		{
			if(card.suit == s)
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks to see if the queen of spades in your hand
	 * 
	 * @param cardsToSearch Set of cards to look through
	 * @return whether or not the queen is in your hand
	 */
	private boolean hasQueenOfSpades(Set<Card> cardsToSearch)
	{
		Set<Card> spades = getCardsBySuit(Suit.SPADE, cardsToSearch);
		if(spades == null)
		{
			return false;
		}
		for(Card c : spades)
		{
			if(c.rank == Rank.QUEEN)
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns a random Card in your hand
	 * 
	 * @param cards Set of Cards to get a random one from
	 * @return random Card
	 */
	private Card getRandomCard(Set<Card> cards)
	{
		if(cards == null || cards.size() == 0)
		{
			return null;
		}
		Card[] cardsArray = new Card[cards.size()];
		cardsArray = cards.toArray(cardsArray);
		return cardsArray[(int) (Math.random() * cardsArray.length)];
	}

	/**
	 * Gets the card with the highest rank in the given set of cards
	 * 
	 * @param cards Set of cards to get the highest one from
	 * @return highest ranked card
	 */
	private Card getHighestCard(Set<Card> cards)
	{
		if(cards == null || cards.size() == 0)
		{
			return null;
		}
		Card highest = null;
		for(Card c : cards)
		{
			if(highest == null)
			{
				highest = c;
			}
			else if(c.rank.compareTo(highest.rank) > 0)
			{
				highest = c;
			}
		}
		return highest;
	}

	/**
	 * Gets the card with the lowest rank in the given set of cards
	 * 
	 * @param cards Set of cards to get the lowest one from
	 * @return lowest ranked card
	 */
	private Card getLowestCard(Set<Card> cards)
	{
		if(cards == null || cards.size() == 0)
		{
			return null;
		}
		Card lowest = null;
		for(Card c : cards)
		{
			if(lowest == null)
			{
				lowest = c;
			}
			else if(c.rank.compareTo(lowest.rank) < 0)
			{
				lowest = c;
			}
		}
		return lowest;
	}

	/**
	 * Finds the highest card of the right suit played during the current trick
	 * 
	 * @return the highest card played
	 */
	private Card getHighestCardPlayedOfRightSuit()
	{
		Set<Card> cardsPlayedSoFar = new HashSet<Card>();
		if(cardsPlayedDuringTrick == null || cardsPlayedDuringTrick.length == 0)
		{
			return null;
		}
		for(int i = 0; i < numCardsPlayed; i++)
		{
			cardsPlayedSoFar.add(cardsPlayedDuringTrick[i]);
		}
		return getHighestCard(getCardsBySuit(cardsPlayedDuringTrick[0].suit, cardsPlayedSoFar));
	}

	/**
	 * Gets a Set of all the cards of one suit in your hand
	 * @param s Suit which you want
	 * @param cardsToSearch Set of cards to look through
	 * @return Set of cards of that Suit in your hand, null if none
	 */
	private Set<Card> getCardsBySuit(Suit s, Set<Card> cardsToSearch)
	{
		if(!hasSuit(s, cardsToSearch))
		{
			return null;
		}

		Set<Card> cardsBySuit = new HashSet<Card>();
		for(Card card:cardsToSearch)
		{
			if(card.suit == s)
			{
				cardsBySuit.add(card);
			}
		}
		return cardsBySuit;
	}

	/**
	 * Gets the number of cards you have of a given Suit in this set.
	 * 
	 * @param s Suit in which you are interested in
	 * @param cardsToSearch Set of cards to look through
	 * @return number of cards of that suit in your hand
	 */
	private int getNumberOfCardsForSuit(Suit s, Set<Card> cardsToSearch)
	{
		Set<Card> cards = getCardsBySuit(s, cardsToSearch);
		if(cards == null)
		{
			return 0;
		}

		return cards.size();
	}

	/**
	 * Gets the cards in the given set that are of a rank lower than or equal to the given card's rank
	 * 
	 * @param card card to get cards lower than
	 * @param cardsToSearch set to search through
	 * @return set of cards lower than or equal to the given card
	 */
	private Set<Card> getCardsLowerThanOrEqualTo(Card card, Set<Card> cardsToSearch)
	{
		if(cardsToSearch == null || cardsToSearch.size() == 0 || card == null)
		{
			return null;
		}
		Set<Card> lowerCards = new HashSet<Card>();
		for(Card c: cardsToSearch)
		{
			if(c.rank.compareTo(card.rank) <= 0)
			{
				lowerCards.add(c);
			}
		}
		return lowerCards;
	}

	/**
	 * Gets the cards in the given set that are of a rank higher than or equal to the given card's rank
	 * 
	 * @param card card to get cards higher than
	 * @param cardsToSearch set to search through
	 * @return set of cards higher than or equal to the given card
	 */
	private Set<Card> getCardsHigherThanOrEqualTo(Card card, Set<Card> cardsToSearch)
	{
		if(cardsToSearch == null || cardsToSearch.size() == 0 || card == null)
		{
			return null;
		}
		Set<Card> higherCards = new HashSet<Card>();
		for(Card c: cardsToSearch)
		{
			if(c.rank.compareTo(card.rank) >= 0)
			{
				higherCards.add(c);
			}
		}
		return higherCards;
	}

	/**
	 * Gets the added value (a 2 is worth 0, an Ace is worth 13) of all the cards of a given suit
	 * 
	 * @param s Suit to get card values from
	 * @param cardsToSearch Set of cards to look through
	 * @return added total of the value of all the cards of that suit
	 */
	private int getValueOfCardsForSuit(Suit s, Set<Card> cardsToSearch)
	{
		Set<Card> cardsBySuit = getCardsBySuit(s, cardsToSearch);
		if(cardsBySuit == null)
		{
			return 0;
		}
		int total = 0;
		for(Card c : cardsBySuit)
		{
			total = total + c.rank.ordinal();
		}
		return total;
	}

	/**
	 * Finds the suit with the lowest representation in your hand
	 * 
	 * @param cardsToSearch Set of cards to look through
	 * 
	 * @return suit with lowest representation
	 */
	private Suit getSuitWithLowestNumberOfCards(Set<Card> cardsToSearch)
	{
		Suit suit = Suit.CLUB;
		for(Suit s:Card.Suit.values())
		{
			if(getNumberOfCardsForSuit(s, cardsToSearch) < getNumberOfCardsForSuit(suit, cardsToSearch))
			{
				suit = s;
			}
		}
		return suit;
	}

	/**
	 * Finds and returns the lowest card that has not been played of the given suit
	 * 
	 * @param s Suit to get lowest card from
	 * @return lowest card yet to be played in the current round of the specified suit
	 */
	private Card getLowestCardYetToBePlayedOfSuit(Suit s)
	{
		return getLowestCard(getCardsBySuit(s, cardsYetToBePlayed));
	}

	/**
	 * Finds the suit (that is not voided) with the lowest representation in your hand
	 * 
	 * @param cardsToSearch Set of cards to look through
	 * 
	 * @return suit with lowest representation (non-zero)
	 */
	private Suit getNonVoidedSuitWithLowestNumberOfCards(Set<Card> cardsToSearch)
	{
		Suit suit = Suit.CLUB;
		for(Suit s:Card.Suit.values())
		{
			if(getNumberOfCardsForSuit(s, cardsToSearch) != 0 && 
					(getNumberOfCardsForSuit(s, cardsToSearch) < getNumberOfCardsForSuit(suit, cardsToSearch) 
							|| getNumberOfCardsForSuit(suit, cardsToSearch) == 0))
			{
				suit = s;
			}
		}
		return suit;
	}


	/**
	 * Gets a Set of all the cards except of one Suit in your hand
	 * 
	 * @param s Suit to exclude
	 * @param cardsToSearch Set of cards to look through
	 * @return Set of all cards except that Suit, null if none
	 */
	private Set<Card> getAllCardsExceptSuit(Suit s, Set<Card> cardsToSearch)
	{
		Set<Suit> suitsToGet = new HashSet<Suit>();
		for(Suit suit:Suit.values())
		{
			if(suit != s)
			{
				suitsToGet.add(suit);
			}
		}

		boolean hasOtherSuit = false;
		for(Suit suit:suitsToGet)
		{
			if(hasSuit(suit, cardsToSearch))
			{
				hasOtherSuit = true;
			}
		}

		if(!hasOtherSuit)
		{
			return null;
		}

		Set<Card> cardsBySuit = new HashSet<Card>();
		for(Card card:cardsToSearch)
		{
			for(Suit suit:suitsToGet)
				if(card.suit == suit)
				{
					cardsBySuit.add(card);
				}
		}
		return cardsBySuit;
	}

	/**
	 * Returns the highest card in the cards given that would be guaranteed to be beaten
	 * 
	 * @param s Suit we are looking at for the highest card
	 * @param cardsToSearch cards to look through
	 * @return highest card of correct suit that will be outplayed (null if non-existent)
	 */
	private Card getHighestSafeCardToPlay(Suit s, Set<Card> cardsToSearch)
	{
		if(s == Suit.SPADE && hasQueenOfSpades(cardsYetToBePlayed))
		{
			return getHighestCard(getCardsLowerThanOrEqualTo(getLowestCardYetToBePlayedOfSuit(s), 
					getCardsBySuit(s, getCardsLowerThanOrEqualTo(new Card(Card.Rank.JACK, 
							Card.Suit.SPADE), cardsToSearch))));
		}
		return getHighestCard(getCardsLowerThanOrEqualTo(getLowestCardYetToBePlayedOfSuit(s), getCardsBySuit(s, cardsToSearch)));
	}

	/**
	 * Returns the total point value of the cards played so far in the current trick
	 * 
	 * @return total point value of trick
	 */
	private int getValueOfTrickSoFar()
	{
		int total = 0;
		if(cardsPlayedDuringTrick == null || cardsPlayedDuringTrick.length == 0)
		{
			return total;
		}
		for(int i = 0; i < numCardsPlayed; i++)
		{
			total = total + cardValue(cardsPlayedDuringTrick[i]);
		}
		return total;
	}

	/**
	 * Returns the value of one card
	 * 
	 * @param c Card to get value of
	 * @return value of Card
	 */
	private int cardValue(Card c)
	{
		if(c.suit == Suit.HEART)
		{
			return 1;
		}
		else if(c.suit == Suit.SPADE && c.rank == Rank.QUEEN)
		{
			return 13;
		}
		return 0;
	}

	private Set<Card> cardsWithout(Card c, Set<Card> cardsToSearch)
	{
		Set<Card> lol = new HashSet<Card>();
		lol.addAll(cardsToSearch);
		lol.remove(c);
		return lol;
	}
}
